package com.ctrip.framework.apollo.internals;

import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.core.dto.ServiceDTO;
import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
import com.ctrip.framework.apollo.tracer.Tracer;
import com.ctrip.framework.apollo.tracer.spi.Transaction;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.ctrip.framework.apollo.util.ExceptionUtil;
import com.ctrip.framework.apollo.util.http.HttpRequest;
import com.ctrip.framework.apollo.util.http.HttpResponse;
import com.ctrip.framework.apollo.util.http.HttpUtil;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.escape.Escaper;
import com.google.common.net.UrlEscapers;
import com.google.gson.reflect.TypeToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicReference;

/**
 * ConfigService 定位器
 */
public class ConfigServiceLocator {

    private static final Logger logger = LoggerFactory.getLogger(ConfigServiceLocator.class);

    private static final Joiner.MapJoiner MAP_JOINER = Joiner.on("&").withKeyValueSeparator("=");
    private static final Escaper queryParamEscaper = UrlEscapers.urlFormParameterEscaper();

    private HttpUtil m_httpUtil;
    private ConfigUtil m_configUtil;
    /**
     * ServiceDTO 数组的缓存
     */
    private AtomicReference<List<ServiceDTO>> m_configServices;
    private Type m_responseType;
    /**
     * 定时任务 ExecutorService
     */
    private ScheduledExecutorService m_executorService;

    /**
     * Create a config service locator.
     */
    public ConfigServiceLocator() {
        List<ServiceDTO> initial = Lists.newArrayList();
        m_configServices = new AtomicReference<>(initial);
        m_responseType = new TypeToken<List<ServiceDTO>>() {
        }.getType();
        m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
        m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
        this.m_executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ConfigServiceLocator", true));
        // 初始拉取 Config Service 地址
        this.tryUpdateConfigServices();
        // 创建定时任务，定时拉取 Config Service 地址
        this.schedulePeriodicRefresh();
    }

    /**
     * Get the config service info from remote meta server.
     *
     * @return the services dto
     */
    public List<ServiceDTO> getConfigServices() {
        // 缓存为空，强制拉取
        if (m_configServices.get().isEmpty()) {
            updateConfigServices();
        }
        // 返回 ServiceDTO 数组
        return m_configServices.get();
    }

    private boolean tryUpdateConfigServices() {
        try {
            updateConfigServices();
            return true;
        } catch (Throwable ex) {
            //ignore
        }
        return false;
    }

    private void schedulePeriodicRefresh() {
        this.m_executorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                logger.debug("refresh config services");
                Tracer.logEvent("Apollo.MetaService", "periodicRefresh");
                // 拉取 Config Service 地址
                tryUpdateConfigServices();
            }
        }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());
    }

    private synchronized void updateConfigServices() {
        // 拼接请求 Meta Service URL
        String url = assembleMetaServiceUrl();

        HttpRequest request = new HttpRequest(url);
        int maxRetries = 2; // 重试两次
        Throwable exception = null;

        // 循环请求 Meta Service ，获取 Config Service 地址
        for (int i = 0; i < maxRetries; i++) {
            Transaction transaction = Tracer.newTransaction("Apollo.MetaService", "getConfigService");
            transaction.addData("Url", url);
            try {
                // 请求
                HttpResponse<List<ServiceDTO>> response = m_httpUtil.doGet(request, m_responseType);
                transaction.setStatus(Transaction.SUCCESS);
                // 获得结果 ServiceDTO 数组
                List<ServiceDTO> services = response.getBody();
                // 获得结果为空，重新请求
                if (services == null || services.isEmpty()) {
                    logConfigService("Empty response!");
                    continue;
                }
                // 更新缓存
                m_configServices.set(services);
                // 打印结果 ServiceDTO 数组
                logConfigServices(services);
                return;
            } catch (Throwable ex) {
                Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
                transaction.setStatus(ex);
                exception = ex;
            } finally {
                transaction.complete();
            }
            // 请求失败，sleep 等待下次重试
            try {
                m_configUtil.getOnErrorRetryIntervalTimeUnit().sleep(m_configUtil.getOnErrorRetryInterval());
            } catch (InterruptedException ex) {
                // ignore
            }
        }
        // 请求全部失败，抛出 ApolloConfigException 异常
        throw new ApolloConfigException(String.format("Get config services failed from %s", url), exception);
    }

    private String assembleMetaServiceUrl() {
        String domainName = m_configUtil.getMetaServerDomainName();
        String appId = m_configUtil.getAppId();
        String localIp = m_configUtil.getLocalIp();

        // 参数集合
        Map<String, String> queryParams = Maps.newHashMap();
        queryParams.put("appId", queryParamEscaper.escape(appId));
        if (!Strings.isNullOrEmpty(localIp)) {
            queryParams.put("ip", queryParamEscaper.escape(localIp));
        }

        return domainName + "/services/config?" + MAP_JOINER.join(queryParams);
    }

    private void logConfigServices(List<ServiceDTO> serviceDtos) {
        for (ServiceDTO serviceDto : serviceDtos) {
            logConfigService(serviceDto.getHomepageUrl());
        }
    }

    private void logConfigService(String serviceUrl) {
        Tracer.logEvent("Apollo.Config.Services", serviceUrl);
    }

}